package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private StringRedisTemplate redisTemplate;

  /* @Override
   public synchronized void testLock() {
      // 查询Redis中的num值
      String value = this.stringRedisTemplate.opsForValue().get("num");
      // 没有该值return
      if (StringUtils.isBlank(value)){
         return ;
      }
      // 有值就转成int
      int num = Integer.parseInt(value);
      // 把Redis中的num值+1
      this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
   }*/


    @Override
    public void testLock() {
        //0.TODO 尝试从缓存中获取业务数据，如果命中缓存则返回即可，缓存未命中，解决缓存击穿问题，先获取分布式锁
        //1.获取分布式锁
        //1.1 版本1：采用Redis提供setnx命令 存在问题：A线程加锁成功后，逻辑执行中服务宕机，锁无法释放造成死锁
        String lockName = "lock";
        //String lockValue = "lock";
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
        //1.2 版本2：在版本1基础上，通过expire命令为锁的Key设置过期时间，防止死锁 问题：加锁跟设置锁过期时间不具备原子性
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
        ////防止死锁，为锁的Key设置过期时间
        //redisTemplate.expire(lockName,5, TimeUnit.SECONDS);
        //1.3 版本3: 使用set命令配合 NX EX 参数 替换版本2的命令 确保 写入值跟设置过期时间原子性 问题：
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
        //1.4 版本4：在版本3基础上，存入Redis锁的值，为每个线程生成UUID作为线程标识。
        String lockValue = UUID.randomUUID().toString();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);
        //2.获取锁成功后，才执行业务逻辑
        if (flag) {
            try {
                // 查询Redis中的num值
                String value = this.redisTemplate.opsForValue().get("num");
                // 没有该值return
                if (StringUtils.isBlank(value)) {
                    return;
                }
                // 有值就转成int
                int num = Integer.parseInt(value);
                // 把Redis中的num值+1
                this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
            } finally {
                //3.TODO 释放锁
                //4.版本4：先判断Redis中锁的值跟当前线程虚拟机栈获取当前线程标识值是否一致，一致则删除锁  问题：判断跟删除锁不是原子操作
                //if (redisTemplate.opsForValue().get(lockName).equals(lockValue)) {
                //    redisTemplate.delete(lockName);
                //}
                //5.版本5：使用lua脚本删除锁，确保删除锁跟判断锁值一致性
                // 问题：1.编码麻烦  2.底层采用String无法锁可重入  3.加锁后没有锁自动续期机制  版本6：采用生产框架Redisson提供分布式锁
                //5.1 创建Redis脚本对象，封装lua脚本
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end\n";
                RedisScript<Boolean> booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class);
                redisTemplate.execute(booleanRedisScript, Arrays.asList(lockName), lockValue);
            }
        } else {
            try {
                log.error("获取锁失败:{},再次尝试自旋", Thread.currentThread().getName());
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            this.testLock();
        }
    }
}
